package com.solt.prism.core.wrapper;

import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import net.xeoh.plugins.base.annotations.PluginImplementation;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.log4j.Logger;
import org.lobobrowser.html.domimpl.HTMLElementImpl;
import org.w3c.dom.DOMException;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.solt.prism.core.conf.Configurable;
import com.solt.prism.core.conf.Configuration;
import com.solt.prism.core.conf.PrismConfiguration;
import com.solt.prism.core.conf.news.Field;
import com.solt.prism.core.conf.news.Filter;
import com.solt.prism.core.entity.News;
import com.solt.prism.core.utils.NodeUtils;
import com.solt.prism.core.utils.PrismConstants;
import com.solt.prism.plugins.NewsWrapper;

@PluginImplementation
public class XPathWrapper implements NewsWrapper, Configurable {
	private static final Logger logger = Logger.getLogger(XPathWrapper.class);
	private static final String IMG_TAG = "img";
	private static final String A_TAG = "a";
	private static final String SCRIPT_TAG = "script";
	private XPath xpath;
	private Configuration conf;
	private List<Field> fields;
	private boolean autoThumbnail;
	private Map<String, List<FilterEngine>> filterOne;
	private Map<String, List<FilterEngine>> filterAll;

	public XPathWrapper() {
		xpath = XPathFactory.newInstance().newXPath();
		autoThumbnail = PrismConfiguration.getInstance().getBoolean(
				PrismConstants.NEWS_AUTO_THUMBNAIL, false);
		filterOne = new HashMap<String, List<FilterEngine>>();
		filterAll = new HashMap<String, List<FilterEngine>>();
	}

	@Override
	public void init(List<Field> fields) {
		this.fields = fields;
		initFilter();
	}

	private void initFilter() {
		List<Filter> filters = null;
		List<FilterEngine> one = null;
		List<FilterEngine> all = null;
		FilterEngine engine = null;
		for (Field field : fields) {
			filters = field.getFilter();
			one = filterOne.get(field.getName());
			if (one == null) {
				one = new ArrayList<FilterEngine>();
				filterOne.put(field.getName(), one);
			}
			all = filterAll.get(field.getName());
			if (all == null) {
				all = new ArrayList<FilterEngine>();
				filterAll.put(field.getName(), all);
			}
			for (Filter filter : filters) {
				if (filter.getType().equals("regexone")) {
					engine = new RegexFilterEngine();
					one.add(engine);
				} else if (filter.getType().equals("scriptone")) {
					engine = new ScriptFilterEngine(conf);
					one.add(engine);
				} else if (filter.getType().equals("regexall")) {
					engine = new RegexFilterEngine();
					all.add(engine);
				} else if (filter.getType().equals("scriptall")) {
					engine = new ScriptFilterEngine(conf);
					all.add(engine);
				}
				engine.init(filter.getValue(), filter.getReplace());
			}
		}
	}

	private void refineImgNodeWithThumbnail(HTMLElementImpl imgNode, News news) {
		String uri = imgNode.getAttribute("src");
		if (uri != null) {
			try {
				URL url = new URL(news.getSourceUrl(), uri);
				imgNode.setAttribute("src", url.toString());
				if (news.getImgUrl() == null && autoThumbnail) {
					news.setImgUrl(url.toString());
				}
			} catch (Exception e) {
				logger.warn("can't refine node: " + imgNode.getOuterHTML(), e);
			}
		}

	}

	protected void refineImgNode(HTMLElementImpl imgNode, URL sourceUrl) {
		String uri = imgNode.getAttribute("src");
		if (uri != null) {
			try {
				URL url = new URL(sourceUrl, uri);
				imgNode.setAttribute("src", url.toString());
			} catch (Exception e) {
				logger.warn("can't refine node: " + imgNode.getOuterHTML(), e);
			}
		}
	}

	
	
	protected void refineHyperLinkNode(HTMLElementImpl aNode, URL url) {
		String uri = aNode.getAttribute("href");
		if (uri != null) {
			try {
				aNode.setAttribute("href", NodeUtils.getURLOnSite(new URL(url, uri)));
			} catch (Exception e) {
				logger.warn("can't refine node: " + aNode.getOuterHTML(), e);
			}
		}
		return;

	}

	protected void removeScriptNode(HTMLElementImpl scriptNode) {
		Node parent = scriptNode.getParentNode();
		parent.removeChild(scriptNode);
	}

	/**
	 * refine a given node. the node should normalize urls inside node.
	 * 
	 * @param node
	 * @param url
	 * @return true if node's ready to using, false if node should was ignore
	 */
	protected boolean refineNodes(HTMLElementImpl node, URL url) {
		if (node.getTagName().equalsIgnoreCase(SCRIPT_TAG)) {
			return false;
		} else if (node.getTagName().equalsIgnoreCase(IMG_TAG)) {
			refineImgNode(node, url);
		} else if (node.getTagName().equalsIgnoreCase(A_TAG)) {
			refineHyperLinkNode(node, url);
		}
		NodeList nodeList = node.getElementsByTagName(IMG_TAG);
		for (int i = 0; i < nodeList.getLength(); ++i) {
			refineImgNode((HTMLElementImpl) nodeList.item(i), url);
		}
		nodeList = node.getElementsByTagName(A_TAG);
		for (int i = 0; i < nodeList.getLength(); ++i) {
			refineHyperLinkNode((HTMLElementImpl) nodeList.item(i), url);
		}
		nodeList = node.getElementsByTagName(SCRIPT_TAG);
		for (int i = 0; i < nodeList.getLength(); ++i) {
			removeScriptNode((HTMLElementImpl) nodeList.item(i));
		}
		return true;
	}

	protected boolean refineNodesWithThumbnail(Node node, News news) {
		if (node.getNodeType() != Node.ELEMENT_NODE) {
			return true;
		}
		HTMLElementImpl eleNode = (HTMLElementImpl) node;
		URL url = news.getSourceUrl();
		if (eleNode.getTagName().equalsIgnoreCase(SCRIPT_TAG)) {
			return false;
		} else if (eleNode.getTagName().equalsIgnoreCase(IMG_TAG)) {
			refineImgNodeWithThumbnail(eleNode, news);
		} else if (eleNode.getTagName().equalsIgnoreCase(A_TAG)) {
			refineHyperLinkNode(eleNode, url);
		}
		NodeList nodeList = eleNode.getElementsByTagName(IMG_TAG);
		for (int i = 0; i < nodeList.getLength(); ++i) {
			refineImgNodeWithThumbnail((HTMLElementImpl) nodeList.item(i), news);
		}
		nodeList = eleNode.getElementsByTagName(A_TAG);
		for (int i = 0; i < nodeList.getLength(); ++i) {
			refineHyperLinkNode((HTMLElementImpl) nodeList.item(i), url);
		}
		nodeList = eleNode.getElementsByTagName(SCRIPT_TAG);
		for (int i = 0; i < nodeList.getLength(); ++i) {
			removeScriptNode((HTMLElementImpl) nodeList.item(i));
		}
		return true;
	}

	protected boolean fillDataWithFilter(News news, String fieldName,
			NodeList itemNode) throws IllegalAccessException,
			InvocationTargetException {
		StringBuilder value = new StringBuilder();
		StringBuilder plainText = new StringBuilder();
		Node node = null;
		String nodeContent = null;
		for (int i = 0; i < itemNode.getLength(); ++i) {
			node = itemNode.item(i);
			if (fieldName.equals("htmlContent")) {
				if (refineNodesWithThumbnail(node, news)) {
					nodeContent = filterOneContent(fieldName,
							NodeUtils.getContent(node), news, node);
					value.append(nodeContent).append('\n');
					plainText.append(NodeUtils.getTextContent(node)).append(
							'\n');
				}
			} else {
				nodeContent = filterOneContent(fieldName,
						NodeUtils.getContent(node), news, node);
				value.append(nodeContent).append('\n');
			}
			if (nodeContent == null) {
				return false;
			}
		}
		String result = filterAllContent(fieldName, value.toString(), news);
		if (result == null) {
			return false;
		}
		BeanUtils.setProperty(news, fieldName, result);
		if (fieldName.equals("htmlContent")) {
			news.setTextContent(plainText.toString());
		}
		return true;
	}

	private String filterOneContent(String fieldName, String textContent,
			News news, Node node) {
		List<FilterEngine> engines = filterOne.get(fieldName);
		for (int i = 0, n = engines.size(); i < n && textContent != null; ++i) {
			textContent = engines.get(i).refine(textContent, news, node);
		}
		return textContent;
	}

	private String filterAllContent(String fieldName, String textContent,
			News news) {
		List<FilterEngine> engines = filterAll.get(fieldName);
		for (int i = 0, n = engines.size(); i < n && textContent != null; ++i) {
			textContent = engines.get(i).refine(textContent, news);
		}
		return textContent;
	}

	@Override
	public boolean extract(Object htmlDom, News news) {
		for (Field field : fields) {
			try {
				NodeList itemNode = (NodeList) xpath.evaluate(field.getXpath(),
						htmlDom, XPathConstants.NODESET);
				if (itemNode == null || itemNode.getLength() == 0) {
					return false;				
				}
				if (!fillDataWithFilter(news, field.getName(), itemNode)) {
					return false;
				}
			} catch (Exception e) {
				logger.error("error when extract data: " + news, e);
				return false;
			}
		}
		logger.debug(news + "\nContent: " + news.getHtmlContent());
		return true;
	}

	@Override
	public void setConf(Configuration conf) {
		this.conf = conf;
	}

	@Override
	public Configuration getConf() {
		return conf;
	}
}
